package raytracer.main;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JRadioButton;

import raytracer.math.Color3d;
import raytracer.math.Vector3d;
import raytracer.misc.AppWideConstants;
import raytracer.misc.KDNode;
import raytracer.misc.Material;
import raytracer.misc.Ray;
import raytracer.objects.LightSource;
import raytracer.objects.SpaceStructure;
import raytracer.objects.ThreeDObject;

/**
 * 
 * @author Thorsten Roth
 * @version 04.11.2006
 */
class Raytracer extends JFrame implements ActionListener {

	private static final long serialVersionUID = 1L;
	private Color3d backgroundColor;
	private int xRes;
	private int yRes;
	private Scene scene;
	private Image outputImage;
	private SpaceStructure ss;
	private KDNode kdTree;
	private int radioOption = 0; //0 = Ohne, 1 = Uniformgrid, 2 = KD-Baum
	private ArrayList<JRadioButton> radioButtons = new ArrayList<JRadioButton>(3);
			
	public Raytracer(int xr,int yr, Scene scene) {
		this.scene=scene;
		this.setResolution(xr,yr);
		backgroundColor=new Color3d(0,0,0);
		
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);
		this.setTitle("ROFLCOPTER");
		this.setResizable(false);
		
		this.setLayout(new FlowLayout());
		this.createButtons();
		this.setVisible(true);
	}
	
	private void createButtons()
	{
		this.radioButtons.add(new JRadioButton("Ohne Structure"));
		this.radioButtons.add(new JRadioButton("Uniformgrid"));
		this.radioButtons.add(new JRadioButton("KD-Baum"));
		int i = 0;
		for(JRadioButton button:radioButtons)
		{
			button.addActionListener(this);
			button.setName(String.valueOf(i));
			i++;
			this.add(button);
		}

	}

	public Raytracer(Scene scene) {
		this(500,500,scene);
	}
	
	public Raytracer() {
		throw new RuntimeException("*** A Scene must be set!");
	}
	
	public void setResolution(int xr, int yr) {
		this.xRes=xr;
		this.yRes=yr;
		this.setSize(new Dimension(xr+6,yr+25));
		scene.getCamera().setRes(xr,yr);
	}
	
	public void paint(Graphics g) {
		g.drawImage(outputImage,3,22,null);
	}
	
	/**
	 * Trace the whole picture!
	 * @param xFactor: x-Factor for AntiAliasing
	 * @param yFactor: y-Factor for AntiAliasing
	 */
	public long tracePicture() {
		double xFactor=AppWideConstants.ANTIALIAS_FACTOR_X;
		double yFactor=AppWideConstants.ANTIALIAS_FACTOR_Y;
		
		if (scene.getCamera()==null)
			throw new RuntimeException("*** No camera has been set!");
		
		Color3d curCol;
		
		scene.getCamera().setRes((int)(xRes*xFactor),(int)(yRes*yFactor));
		BufferedImage tempImage=new BufferedImage((int)(xRes*xFactor),(int)(yRes*yFactor),BufferedImage.TYPE_INT_RGB);
		
		int percentage=0;
		
		int yRealRes=(int)(yRes*yFactor);
		int xRealRes=(int)(xRes*xFactor);
		int onePercent=yRealRes/100;
		
		long startTime=System.currentTimeMillis();
		
		//ss=new UniformGrid(scene);
		ss = KDNode.createKDTree(scene.getObjectList(), 0);

		
		for (int y=0;y<yRealRes;y++) {
			for (int x=0;x<xRealRes;x++) {
				Ray currentRay=scene.getCamera().getRay(x,y);
				curCol=raytrace(currentRay,0);
				Color correctedColor=c3fToColor(curCol);
				
				/* Create the RGB value needed for setRGB */
				int color=0x000000
					    |correctedColor.getRed()<<16
					    |correctedColor.getGreen()<<8
					    |correctedColor.getBlue();
								
				tempImage.setRGB(x,y,color);
				outputImage=tempImage.getScaledInstance(xRes,yRes,BufferedImage.SCALE_SMOOTH);
				this.repaint();
			}	
			
			if (y%onePercent==0) {
				if (++percentage%1==0)
					this.setTitle("Rendering..."+percentage+"%");
			}
		}
		long endTime=System.currentTimeMillis()-startTime;
		this.setTitle(this.getTitle()+" (took "+((float)endTime/1000)+"s)");
		return endTime;
	}
	
		
	/**
	 * Converts Color3d to a java.awt.Color
	 * Note: This will cut values above 1
	 * @param c3f Some Color3d
	 * @return the java.awt.Color created from the input
	 */
	private Color c3fToColor(Color3d c3f) {
		c3f.x=Math.min(c3f.x,1);
		c3f.y=Math.min(c3f.y,1);
		c3f.z=Math.min(c3f.z,1);
		c3f.x=Math.max(c3f.x,0);
		c3f.y=Math.max(c3f.y,0);
		c3f.z=Math.max(c3f.z,0);
		return new Color((float)c3f.x,(float)c3f.y,(float)c3f.z);
	}
	

	
	private Color3d raytrace(Ray ray, int depth) {
		if (depth>AppWideConstants.MAXDEPTH) {
			return new Color3d(0,0,0);
		}
		else {
			boolean intersect=false;
			Double t=null;
			Double minDistance=Double.MAX_VALUE;
			ThreeDObject intersectedObject=null;
			Vector3d intersectionPoint=new Vector3d();
			Vector3d intersectionNormal=new Vector3d();
			Vector3d tempIntersectionNormal=new Vector3d();
			Vector3d tempIntersectionPoint=new Vector3d();
			Color3d iDiffuse=new Color3d();
			Color3d iSpecular=new Color3d();
			
			/* Use the space structure for intersection */
			if ((intersectedObject=ss.intersect(ray, intersectionPoint, intersectionNormal))!=null) {
					intersect=true;
			}
					
			/* ***REPLACED BY SPACE STRUCTURE ***
			 * Check all objects for intersection with the ray
			 */
//			for (ThreeDObject obj : scene.getObjectList()) {
//				if ((t=obj.intersect(ray,tempIntersectionPoint,tempIntersectionNormal))!=null) {
//					if (t<minDistance && t-AppWideConstants.EPSILON>0) {
//						intersect=true;
//						intersectionPoint=new Vector3d(tempIntersectionPoint);
//						intersectionNormal=new Vector3d(tempIntersectionNormal);
//						minDistance=t;
//						intersectedObject=obj;
//					}
//				}
//			}
			
			
//			if((intersectedObject=kdTree.intersect(ray))!= null){
//				intersect=true;
//			}
				
			
			if (!intersect) {
				return new Color3d(backgroundColor);
			}
			else {
				/* This is needed for the triangles to be lightened correctly from both sides */
				//if (intersectedObject instanceof Triangle || intersectedObject instanceof Plane) {
					if (intersectionNormal.dot(ray.getDirection())>0) {
						intersectionNormal.negate();
					}
				//}
				
				/* Continue with the nearest intersected object */								
				Material intersectedMaterial=intersectedObject.getMaterial();
				
				/* Total color is set to the base value of the ambient color */
				Color3d totalColor=new Color3d(intersectedMaterial.getLocalColor());
				if (AppWideConstants.PHONG) {
					totalColor.scale(intersectedMaterial.getKLocal());
				}
								
				/* Initialize the variables needed for calculation of the shadow color */
				Color3d shadowColor=new Color3d(1,1,1);
				
				/* Process all light sources for local lighting */
				for (LightSource l:scene.getLightList()) {
					
					shadowColor.set(1,1,1);
					
					/* First check if the pixel can be reached by the light source */
					Vector3d shadowRayTarget=new Vector3d(l.getPosition());
					shadowRayTarget.sub(intersectionPoint);
					shadowRayTarget.normalize();
					Ray shadowRay=new Ray(new Vector3d(intersectionPoint),shadowRayTarget);
					
					Double shadowT=null;
										
					if (AppWideConstants.SHADOWS) {
						/* Check the blocker object */
						if (l.getBlockerObject()!=null
							&& (shadowT=l.getBlockerObject().intersect(shadowRay,null,null))!=null
							&& shadowT-AppWideConstants.EPSILON>0) {
							
							shadowColor.set(0,0,0);
						}
						/* Blocker object not hit -> check the other objects */
						else {
							
							
							
							ThreeDObject sObj=ss.intersect(shadowRay);
							if (sObj!=null) {
								if (sObj.getMaterial().getKTransparent()==0) {
									l.setBlockerObject(sObj);
									shadowColor.set(0,0,0);
								}
								/* We just consider the nearest object's transparency
								 * maybe change that in future versions */
								else if (AppWideConstants.TRANSPARENCY) {
									shadowColor.scale(sObj.getMaterial().getKTransparent());
									shadowColor.scale(sObj.getMaterial().getDiffuseColor());
								}
							}
							/* Obsolete Code, replaced by SpaceStructure */
//							Double minT = null;
//							for (ThreeDObject sObj : scene.getObjectList()) {
//								if (sObj!=l.getBlockerObject()) {
//									shadowT=sObj.intersect(shadowRay,null,null);
//									if (shadowT!=null && shadowT-AppWideConstants.EPSILON>0) {
//										/* We use the object with minimum distance and no transparency
//										 * as the blocker object which is cached */
//										
//										if (sObj.getMaterial().getKTransparent()==0 && (minT==null || shadowT<minT)) {
//											minT=new Double(shadowT);
//											l.setBlockerObject(sObj);
//										}
//										
//										if (AppWideConstants.TRANSPARENCY) {
//											shadowColor.scale(sObj.getMaterial().getDiffuseColor());
//											shadowColor.scale(sObj.getMaterial().getKTransparent());
//										}
//										else {
//											shadowColor.set(0,0,0);
//										}
//									}
//								}
//							}
						}
					}
					
					if (AppWideConstants.PHONG && (shadowT==null || shadowColor.x>0 || shadowColor.y>0 || shadowColor.z>0)) { // if there's a black shadow, no phong calculations will be done
						
						/* calculate the phong model for this light source and object */
						iDiffuse=new Color3d(intersectedMaterial.getDiffuseColor());
						iDiffuse.scale(intersectedMaterial.getKDiffuse());
						iSpecular=new Color3d(intersectedMaterial.getSpecularColor());
						iSpecular.scale(intersectedMaterial.getKSpecular());
												
						/* Diffuse color */			
						/* Get the direction from the intersected point *
						 * to the light source and then check the angle */
						Vector3d lightPointer=new Vector3d(shadowRay.getDirection());
						float angle=Math.max((float)intersectionNormal.dot(lightPointer),0);
						/* Consider the light source's color and intensity */
						iDiffuse.scale(l.getColor());
						iDiffuse.scale(angle*l.getIntensity());
						iDiffuse.scale(shadowColor);
												
						/* Specular color */
						/* Calculate the reflected ray and check the angle between this	*
						 * and the viewing angle of the camera */
						Vector3d lightReflector=new Vector3d(intersectionNormal);
						lightReflector.scale(2*intersectionNormal.dot(lightPointer));
						lightReflector.sub(lightPointer);
						lightReflector.normalize();
						Vector3d toOrigin=new Vector3d(ray.getOrigin());
						toOrigin.sub(intersectionPoint);
						toOrigin.normalize();
						angle=(float)Math.max(lightReflector.dot(toOrigin),0);
						float scaleFactor=(float)Math.pow(angle,intersectedMaterial.getSpecPower());
						/* Consider the light source's color and intensity */
						iSpecular.scale(l.getColor());
						iSpecular.scale(scaleFactor*l.getIntensity());
						iSpecular.scale(shadowColor);
						
						/* Calculate the total pixel color resulting from the phong model */
						totalColor.add(iDiffuse);	
						totalColor.add(iSpecular);
					}
					else {
						totalColor.scale(shadowColor);
					}
				}
				
				
				
				if (AppWideConstants.AMBIENTOCCLUSION) {
					/* Get samples for AO */
					Vector3d y=new Vector3d(intersectionNormal);
					Vector3d x=y.getOrthogonal();
					Vector3d z=x.cross(y);
					double ambientValue=0;
											
					for (int i=0;i<AppWideConstants.AO_SAMPLES;i++) {
						Vector3d hemispherePoint=Vector3d.getHemispherePoint(x,y,z);
										
						Ray hemisphereRay=new Ray(intersectionPoint,hemispherePoint);
						boolean aoIntersect=false;
						double innerTransparency;
						if (AppWideConstants.TRANSPARENCY) {
							innerTransparency=-1;
						}
						else {
							innerTransparency=0;
						}
						
						ThreeDObject hObj=ss.intersect(hemisphereRay);
						if (hObj!=null) {
							innerTransparency=hObj.getMaterial().getKTransparent()*hemispherePoint.dot(y);
							aoIntersect=true;
						}
						
						/* Obsolete Code, replaced by SpaceStructure */
//						for (ThreeDObject hObj:scene.getObjectList()) {
//							if (hObj!=intersectedObject) {
//								Double distance;
//								if ((distance=hObj.intersect(hemisphereRay,null,null))!=null) {
//									if (distance-AppWideConstants.EPSILON>0) {
//										aoIntersect=true;
//										if (innerTransparency==-1 || innerTransparency>hObj.getMaterial().getKTransparent()) {
//											innerTransparency=hObj.getMaterial().getKTransparent()*hemispherePoint.dot(y);
//										}
//									}
//								}
//							}
//						}
						
						if (!aoIntersect) {
							ambientValue+=(y.dot(hemispherePoint));
						}
						else {
							ambientValue+=(y.dot(hemispherePoint)*innerTransparency);
						}
						
					}
					ambientValue/=AppWideConstants.AO_SAMPLES;
					
					totalColor.scale(ambientValue);
				}
				
				
				
				
														
				/* Is the intersected material reflective? Then calculate the reflected ray	*
				 * and trace it! */
				if (AppWideConstants.REFLECTIONS && intersectedMaterial.getKReflective()>0) {
					/* We calculate the reflected ray
					 * where R=2*<D,N>*N-D and D is the direction of the ray's origin */
					Vector3d reflTarget=new Vector3d(intersectionNormal);
					Vector3d rayOriginDirection=new Vector3d(ray.getOrigin());
					rayOriginDirection.sub(intersectionPoint);
					rayOriginDirection.normalize();
					reflTarget.scale(2*intersectionNormal.dot(rayOriginDirection));
					reflTarget.sub(rayOriginDirection);
					Ray reflectedRay=new Ray(
						intersectionPoint,
						reflTarget
					);
					
					Color3d reflColor=raytrace(reflectedRay,depth+1);
					/* Attenuate the reflection color so it matches the chosen material */
					reflColor.scale(intersectedMaterial.getKReflective());
					totalColor.add(reflColor);
				}
				
				/* Is the intersected material transparent? Then calculate the refracted ray *
				 * and trace it! */
				if (AppWideConstants.TRANSPARENCY && intersectedMaterial.getKTransparent()>0) {
					Vector3d refrTarget=new Vector3d(intersectionNormal);
					Vector3d tempRay=new Vector3d(ray.getDirection());
					tempRay.negate();
					double ni=intersectionNormal.dot(tempRay);
					double refrRatio;
					
					/* Experimental! */
					if (ray.getRefrIndex()==1)
						refrRatio=ray.getRefrIndex()/intersectedMaterial.getRefrIndex();
					else
						refrRatio=ray.getRefrIndex();
					
					refrTarget.scale(
							refrRatio*ni - Math.sqrt(1-(refrRatio*refrRatio)*(1-(ni*ni)))
					);
					tempRay.scale(refrRatio);
					refrTarget.sub(tempRay);
					Ray refractedRay=new Ray(
							intersectionPoint,
							refrTarget
					);
					
					
					/* Experimental! */
					if (refractedRay.getRefrIndex()==1)
						refractedRay.setRefrIndex(intersectedMaterial.getRefrIndex());
					else
						refractedRay.setRefrIndex(1);
										
					Color3d refrColor=raytrace(refractedRay,depth+1);
					//System.out.println(refrColor);
					/*refrColor.scale(diffuseScaleColor);*/
					refrColor.scale(intersectedMaterial.getDiffuseColor());
					refrColor.scale(intersectedMaterial.getKTransparent());
					/* Attenuate the transparency color so it matches the chosen material */
					totalColor.scale(1-intersectedMaterial.getKTransparent());
					totalColor.add(refrColor);
					//totalColor.scale(shadowColor);
				}
				
				return totalColor;
			}
		}
	}

	@Override
	public void actionPerformed(ActionEvent e)
	{
		JRadioButton source = (JRadioButton) e.getSource();
		String buttonName = source.getName();
		for(JRadioButton button:this.radioButtons)
		{
			if(!button.getName().equals(buttonName))
			{
				button.setSelected(false);
			}
		}
		this.radioOption = Integer.valueOf(buttonName);
	}
}